/*
 * Copyright (C) 2010-2011 Comcast Cable.  All Rights Reserved.
 */
package com.hercules.android.receiver.handlers.events.listener;

import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnKeyListener;

import com.hercules.android.receiver.AndroidReceiver;
import com.hercules.android.receiver.DebugHelper;
import com.hercules.android.receiver.LogUtil;
import com.hercules.android.receiver.handlers.events.EventRaiser;
import com.hercules.android.receiver.handlers.events.EventRaisingException;
import com.hercules.android.receiver.handlers.events.EventsUtil;
import com.hercules.android.receiver.handlers.events.KeyMappingFileLoadingException;
import com.hercules.android.receiver.handlers.events.KeyMappingService;
import com.hercules.android.receiver.model.events.XREEventPhase;
import com.hercules.android.receiver.model.events.XREKeyboardEvent;
import com.hercules.android.receiver.model.events.XREVirtualKeyCode;
import com.hercules.android.receiver.model.objects.XREObject;

import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

/**
 * <p>
 * This is the listener that converts an Android key event to a XREKeyboardEvent, then raises the preview and actual
 * events respectively. It only reacts to the event whose type is given by super.eventType
 * </p>
 * <p>
 * <strong>Thread safety: </strong> This class is thread-safe because the base class is thread-safe, the
 * KeyMappingService is thread-safe and the class is immutable.
 * </p>
 *
 * @author mekanizumu, jaindsonvs
 * @author 7realm
 * @version 1.0
 */
@SuppressWarnings("boxing")
public class KeyboardEventListener extends BaseListener implements OnKeyListener {
    /**
     * <p>
     * The logger used for logging.
     * </p>
     * <p>
     * Initialization and Mutability: It is final and won't change once it is initialized as part of variable
     * declaration to: Logger.getLogger(KeyboardEventListener.class.getName()).
     * </p>
     * <p>
     * Usage: It's is used in all public methods.
     * </p>
     */
    private static final Logger LOGGER = Logger.getLogger(KeyboardEventListener.class.getSimpleName());

    /**
     * <p>
     * Represents the default mapping from raw codes to XREVirtualKeyCode objects.
     * </p>
     */
    private static final Map<Integer, XREVirtualKeyCode> DEFAULT_XRE_VIRTUAL_KEY_CODE =
        new HashMap<Integer, XREVirtualKeyCode>();

    {
        // Adds the mapping for general keys.
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_HOME, XREVirtualKeyCode.HOME);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_BACK, XREVirtualKeyCode.BACK);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_STAR, XREVirtualKeyCode.STAR);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_POUND, XREVirtualKeyCode.POUND);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_DPAD_UP, XREVirtualKeyCode.UP);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_DPAD_DOWN, XREVirtualKeyCode.DOWN);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_DPAD_LEFT, XREVirtualKeyCode.LEFT);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_DPAD_RIGHT, XREVirtualKeyCode.RIGHT);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_VOLUME_UP, XREVirtualKeyCode.VOLUME_UP);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_VOLUME_DOWN, XREVirtualKeyCode.VOLUME_DOWN);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_COMMA, XREVirtualKeyCode.COMMA);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_PERIOD, XREVirtualKeyCode.PERIOD);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_TAB, XREVirtualKeyCode.TAB);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_SPACE, XREVirtualKeyCode.SPACE);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_ENTER, XREVirtualKeyCode.ENTER);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_DEL, XREVirtualKeyCode.DELETE);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_MINUS, XREVirtualKeyCode.MINUS);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_EQUALS, XREVirtualKeyCode.EQUAL);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_LEFT_BRACKET, XREVirtualKeyCode.LEFTBRACKET);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_RIGHT_BRACKET, XREVirtualKeyCode.RIGHTBRACKET);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_BACKSLASH, XREVirtualKeyCode.BACKSLASH);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_SEMICOLON, XREVirtualKeyCode.SEMICOLON);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_SLASH, XREVirtualKeyCode.SLASH);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_MENU, XREVirtualKeyCode.MENU);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, XREVirtualKeyCode.PAUSE);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_MEDIA_STOP, XREVirtualKeyCode.STOP);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_MEDIA_NEXT, XREVirtualKeyCode.NEXT);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_MEDIA_REWIND, XREVirtualKeyCode.REWIND);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, XREVirtualKeyCode.FAST_FORWARD);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_MUTE, XREVirtualKeyCode.MUTE);
    }

    {
        // Adds the mapping for letters.
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_A, XREVirtualKeyCode.A);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_B, XREVirtualKeyCode.B);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_C, XREVirtualKeyCode.C);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_D, XREVirtualKeyCode.D);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_E, XREVirtualKeyCode.E);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_F, XREVirtualKeyCode.F);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_G, XREVirtualKeyCode.G);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_H, XREVirtualKeyCode.H);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_I, XREVirtualKeyCode.I);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_J, XREVirtualKeyCode.J);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_K, XREVirtualKeyCode.K);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_L, XREVirtualKeyCode.L);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_M, XREVirtualKeyCode.M);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_N, XREVirtualKeyCode.N);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_O, XREVirtualKeyCode.O);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_P, XREVirtualKeyCode.P);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_Q, XREVirtualKeyCode.Q);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_R, XREVirtualKeyCode.R);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_S, XREVirtualKeyCode.S);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_T, XREVirtualKeyCode.T);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_U, XREVirtualKeyCode.U);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_V, XREVirtualKeyCode.V);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_W, XREVirtualKeyCode.W);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_X, XREVirtualKeyCode.X);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_Y, XREVirtualKeyCode.Y);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_Z, XREVirtualKeyCode.Z);
    }

    {
        // Adds the mapping for digits.
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_0, XREVirtualKeyCode.NUMBER_0);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_1, XREVirtualKeyCode.NUMBER_1);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_2, XREVirtualKeyCode.NUMBER_2);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_3, XREVirtualKeyCode.NUMBER_3);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_4, XREVirtualKeyCode.NUMBER_4);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_5, XREVirtualKeyCode.NUMBER_5);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_6, XREVirtualKeyCode.NUMBER_6);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_7, XREVirtualKeyCode.NUMBER_7);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_8, XREVirtualKeyCode.NUMBER_8);
        DEFAULT_XRE_VIRTUAL_KEY_CODE.put(KeyEvent.KEYCODE_9, XREVirtualKeyCode.NUMBER_9);
    }

    /**
     * <p>
     * The KeyMappingService used to get the virtual key code of raw key code.
     * </p>
     * <p>
     * Initialization and Mutability: It's initialized within constructor, won't change afterwards.
     * </p>
     * <p>
     * Usage: It is used in onKey(), KeyboardEventListener() (for initialization).
     * </p>
     */
    private final KeyMappingService keyMappingService;

    /**
     * <p>
     * Create an instance of the class.
     * </p>
     *
     * @param eventRaiser the EventRaiser used to raise a XRE event.
     * @param source the source of the event.
     * @param receiver the receiver that will be passed to Android listeners of this component.
     * @param keyMappingService the KeyMappingService used to get the virtual key code of raw key code.
     * @param eventType the event type that the listener will react to.
     * @throws IllegalArgumentException if receiver doesn't conform to the following legal value specification: It
     * cannot be null. Or source doesn't conform to the following legal value specification: It cannot be null. Or
     * eventType doesn't conform to the following legal value specification: It cannot be null or empty. Or eventRaiser
     * doesn't conform to the following legal value specification: It cannot be null. Or keyMappingService doesn't
     * conform to the following legal value specification: It cannot be null.
     */
    public KeyboardEventListener(AndroidReceiver receiver, XREObject source, String eventType, EventRaiser eventRaiser,
        KeyMappingService keyMappingService) {
        super(receiver, source, eventType, eventRaiser);

        EventsUtil.checkArgumentNull(keyMappingService, "keyMappingService");

        this.keyMappingService = keyMappingService;
    }

    /**
     * <p>
     * This method is called when a key is stroke on the Android receiver.
     * </p>
     * <p>
     * AndroidAssembly1 method was changed, added preview events
     * <p>
     * AndroidAssembly2 changed to support multiple event handlers, getEventType is not used
     *
     * @param keyCode the code for the physical key that was pressed.
     * @param v the view the key has been dispatched to.
     * @param event the KeyEvent object containing full information about the event.
     * @return always true
     * @throws IllegalArgumentException if v is null; Or event is null;
     */
    public boolean onKey(View v, int keyCode, KeyEvent event) {
        final String methodName = "onKey";
        LogUtil.enter(LOGGER, this, methodName, v, keyCode, event);
        
        try {
            // check arguments
            EventsUtil.checkArgumentNull(v, "v");
            EventsUtil.checkArgumentNull(event, "event");
        } catch (IllegalArgumentException e) {
            throw LogUtil.error(LOGGER, this, methodName, e);
        }

        String eventName;
        switch (event.getAction()) {
        case KeyEvent.ACTION_UP:
            eventName = XREKeyboardEvent.ON_KEY_UP_EVENT_NAME;
            break;
        case KeyEvent.ACTION_DOWN:
            eventName = XREKeyboardEvent.ON_KEY_DOWN_EVENT_NAME;
            break;
        case KeyEvent.ACTION_MULTIPLE:
            eventName = XREKeyboardEvent.ON_KEY_START_REPEATING_EVENT_NAME;
            break;
        default:
            LogUtil.warn(LOGGER, this, methodName, "OnKey action {0} is not supported", event.getAction());
            return true;
        }

        LogUtil.info(LOGGER, this, methodName, "OnKey key code 0x{0}, action {1}; event name {2}, source {3}", Integer
            .toHexString(keyCode), event.getAction(), eventName, getSource().getId());

        boolean isHandled = false;
        try {
            // try to raise event
            isHandled = raiseEvent(event, eventName);
        } catch (EventRaisingException e) {
            LogUtil.error(LOGGER, getClass(), methodName, e);
        } catch (KeyMappingFileLoadingException e) {
            LogUtil.error(LOGGER, getClass(), methodName, e);
        }

        return LogUtil.exit(LOGGER, this, methodName, isHandled);
    }

    /**
     * <p>
     * Tries to raise two {@link XREKeyboardEvent} events.
     * </p>
     * <p>
     * AndroidAssembly1 refactoring
     *
     * @param event the KeyEvent object containing full information about the event.
     * @param eventName the name of the event received.
     * @return true if event was successfully raised
     * @throws EventRaisingException if error occurred while raising event
     * @throws KeyMappingFileLoadingException if error occurred while getting virtual key code
     */
    private boolean raiseEvent(KeyEvent event, String eventName) throws EventRaisingException,
        KeyMappingFileLoadingException {
        // prepare event
        XREKeyboardEvent xreEvent = new XREKeyboardEvent();
        xreEvent.setRawCode(event.getKeyCode());
        xreEvent.setSource(getSource().getId());

        boolean isHandled = false;

        // try to populate virtual key code
        if (populateVirtualKeyCode(event, xreEvent)) {
            LogUtil.info(LOGGER, this, "onKey", "Virtual key code for {0} = {1}", event.getKeyCode(), xreEvent
                .getVirtualKeyCode());

            // sets the properties of xreEvent to be a PREVIEW event and raise it
            xreEvent.setPhase(XREEventPhase.PREVIEW);
            setNextEventIndex(xreEvent);
            xreEvent.setName(eventName.substring(0, 2) + "Preview" + eventName.substring(2));
            isHandled = getEventRaiser().raiseEvent(xreEvent, getReceiver(), false);
            // AndroidAssembly3 if PREVIEW event is handled, then STANDARD is not raised
            if (isHandled) {
            //    return true;
            }

            // sets the event as a STANDARD event and raise it
            xreEvent.setPhase(XREEventPhase.STANDARD);
            xreEvent.setName(eventName);
            setNextEventIndex(xreEvent);
            isHandled = getEventRaiser().raiseEvent(xreEvent, getReceiver(), false);
        }
        return isHandled;
    }

    /**
     * Returns the proper XREVirtualKeyCode based on the raw code and modifiers.
     * <p>
     * AndroidAssembly1 refactoring
     *
     * @param event the KeyEvent object containing full information about the event
     * @param xreEvent the event that will be raised
     * @return true if virtual key code was populated
     * @throws KeyMappingFileLoadingException if any error occurs while loading mapping file
     */
    private boolean populateVirtualKeyCode(KeyEvent event, XREKeyboardEvent xreEvent)
        throws KeyMappingFileLoadingException {
        // check if there is a mapping
        XREVirtualKeyCode virtualKeyCode =
            keyMappingService.getVirtualKeyCode(event.getKeyCode(), event.isShiftPressed(), event.isSymPressed(), event
                .isAltPressed(), event.getMetaState() != 0);

        // try get event virtual code from default mapping
        if (virtualKeyCode == null) {
            virtualKeyCode = DEFAULT_XRE_VIRTUAL_KEY_CODE.get(event.getKeyCode());

            // return false of no mapping
            if (virtualKeyCode == null) {
                return false;
            }

            // set the modifiers for default mapping
            xreEvent.setAlt(event.isAltPressed());
            xreEvent.setControl(event.isSymPressed());
            xreEvent.setShift(event.isShiftPressed());
            xreEvent.setMeta(event.getMetaState() != 0);
        }

        // set virtual key code
        xreEvent.setVirtualKeyCode(virtualKeyCode);
        return true;
    }
}
